/**
 * @license
 * Copyright 2016 E2EMail authors. All rights reserved.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * @fileoverview Service providing access to the OpenPGP library.
 */
goog.provide('e2email.components.openpgp.OpenPgpService');
goog.provide('e2email.components.openpgp.OpenPgpService.DecryptResult');
goog.provide('e2email.components.openpgp.module');

goog.require('e2e');
goog.require('e2e.async.Worker');
goog.require('e2e.cipher.Algorithm');
goog.require('e2e.error.InvalidArgumentsError');
goog.require('e2e.error.UnsupportedError');
goog.require('e2e.openpgp.KeyGenerator');
goog.require('e2e.openpgp.WorkerContextImpl');
goog.require('e2e.openpgp.asciiArmor');
goog.require('e2e.signer.Algorithm');
/** @suppress {extraRequire} import typedef */
goog.require('e2email.util.Progress');
goog.require('e2email.util.RecoveryCode');
goog.require('goog.array');

goog.scope(function() {



/**
 * Service to manage interacting with the E2E OpenPGP library.
 * @param {!angular.$q} $q The angular $q service.
 * @param {!angular.$http} $http The angular $http service.
 * @param {!e2email.components.translate.TranslateService} translateService
 * @param {!e2email.components.appinfo.AppinfoService} appinfoService
 *     the appinfo service.
 * @ngInject
 * @constructor
 */
e2email.components.openpgp.OpenPgpService = function(
    $q, $http, translateService, appinfoService) {
  /** @private */
  this.q_ = $q;
  /** @private */
  this.http_ = $http;
  /** @private */
  this.translateService_ = translateService;
  /** @private */
  this.appinfoService_ = appinfoService;
  /** @private {e2e.openpgp.WorkerContextImpl} */
  this.context_ = null;
  /**
   * Contains a map of public key fingerprints to timestamps. The
   * timestamp is the epoch milliseconds when a locally stored public
   * key was last fetched from the remote keyserver, and is used to
   * decide whether a locally stored key should be reverified from the
   * server.
   * @private {!Object<string,number>}
   */
  this.publicKeyVerificationTimestamps_ = {};
};


/**
 * Structure used to return decryption results. An optional
 * warning is present if (for example) the contents were not
 * signed by the provided key.
 * @typedef {{
 *   content: string,
 *   warning: ?string
 * }}
 */
e2email.components.openpgp.OpenPgpService.DecryptResult;


var OpenPgpService = e2email.components.openpgp.OpenPgpService;


/**
 * Number of milliseconds to wait before refetching a key from
 * the key server.
 * @const
 * @private
 */
OpenPgpService.PUBLIC_KEY_REFETCH_MSEC_ = 60 * 60 * 1000; // 1 hour.


/**
 * Version header name to add to ascii-armored content generated by
 * the application.
 * @const
 * @private
 */
OpenPgpService.VERSION_ = 'Version';


/**
 * @define {string} Keyserver URL, directly used by the service to upload keys.
 */
OpenPgpService.KEYSERVER_URL = '';


/**
 * @define {string} Path to a binary bootstrapping End-to-End Worker.
 */
OpenPgpService.WORKER_BINARY_PATH = '';


/**
 * Initializes the service asynchronously with a passphrase.
 * @param {string} password A passphrase for the keyring.
 * @return {!angular.$q.Promise} when all initializations are
 *     complete.
 */
OpenPgpService.prototype.initialize = function(password) {
  // Check if we need to do anything.
  if (goog.isDefAndNotNull(this.context_)) {
    return this.q_.when(undefined);
  }

  var deferred = this.q_.defer();
  try {
    var peer = new e2e.async.Worker(
        e2email.components.openpgp.OpenPgpService.WORKER_BINARY_PATH);

    e2e.openpgp.WorkerContextImpl.launch(peer)
        .addCallbacks(function(contextImpl) {
          this.context_ = contextImpl;
          deferred.resolve(null);
        }, goog.bind(deferred.reject, deferred), this);
  } catch (any) {
    deferred.reject(any);
  }
  // Return a chained promise that sets the password on the context
  // and then sets a suitable header for ascii-armored content generated
  // for this application.
  return deferred.promise.then(goog.bind(this.unlock_, this, password))
      .then(goog.bind(this.setHeader_, this));
};


/**
 * Sets the passphrase for the context.
 * @param {string} password A passphrase for the keyring.
 * @return {!angular.$q.Promise} resolves when the service is initialized.
 * @private
 */
OpenPgpService.prototype.unlock_ = function(password) {
  var deferred = this.q_.defer();
  this.context_.initializeKeyRing(password).addCallbacks(
      deferred.resolve, deferred.reject, deferred);
  return deferred.promise;
};


/**
 * Sets the the header on ascii-armored content created by the
 * context.
 * @return {!angular.$q.Promise} resolves when the header is set.
 * @private
 */
OpenPgpService.prototype.setHeader_ = function() {
  var header = this.translateService_.getMessage('appName') + ' ' +
      this.appinfoService_.getVersion();
  var deferred = this.q_.defer();
  this.context_.setArmorHeader(OpenPgpService.VERSION_, header)
      .addCallbacks(deferred.resolve, deferred.reject, deferred);
  return deferred.promise;
};


/**
 * Fetch a local private key (if one exists.)
 * @param {string} email An email address to search.
 * @return {!angular.$q.Promise<e2e.openpgp.Key>} A promise that returns
 *     the key if found, and promise returns null otherwise.
 */
OpenPgpService.prototype.searchPrivateKey = function(email) {
  var result = this.context_.searchPrivateKey(this.toUserId_(email));
  var deferred = this.q_.defer();
  result.addCallback(function(keys) {
    if (goog.isDefAndNotNull(keys) && (keys.length === 1)) {
      deferred.resolve(keys[0]);
    } else if (goog.isDefAndNotNull(keys) && (keys.length > 1)) {
      // We don't expect to be in this situation.
      deferred.reject(new e2e.error.UnsupportedError(
          'This application does not support multiple private keys.'));
    } else {
      deferred.resolve(null);
    }
  }).addErrback(deferred.reject, deferred);
  return deferred.promise;
};


/**
 * Returns a promise with a secret code that can regenerate EC keys.
 * @param {string} email An email used to ensure there is a private key
 *     with that address.
 * @return {!angular.$q.Promise<string>}
 */
OpenPgpService.prototype.getSecretBackupCode = function(email) {
  var deferred = this.q_.defer();
  this.context_.getKeyringBackupData().addCallbacks(function(data) {
    if (data.count !== 2) {
      deferred.reject(new e2e.error.UnsupportedError(
          'This app only manages one private key.'));
    } else if (data.seed.length !== e2e.openpgp.KeyGenerator.ECC_SEED_SIZE) {
      deferred.reject(new e2e.error.UnsupportedError(
          'This application does not support this seed size.'));
    } else {
      deferred.resolve(e2email.util.RecoveryCode.encode(data.seed));
    }
  }, goog.bind(deferred.reject, deferred), this);
  return this.searchPrivateKey(email).then(goog.bind(function(key) {
    if (goog.isDefAndNotNull(key)) {
      return deferred.promise;
    } else {
      return this.q_.reject(new e2e.error.InvalidArgumentsError(
          'No private key found.'));
    }
  }, this));
};


/**
 * Restores a keyring using a previously generated secret backup code.
 * If a code is valid (meaning it has a good checksum) it is assumed
 * to represent a key that the user wants to use for the account,
 * regardless of any previous key present locally or remotely.
 * Once restored, the key is sent to the server if necessary.
 * @param {string} code The recovery code.
 * @param {string} email The email associated with the code.
 * @param {string} token An OAuth access token needed to upload keys.
 * @param {e2email.util.Progress} progress A progress object for notifications.
 * @return {!angular.$q.Promise} A promise that resolves when the keyring
 *     has been restored, or rejects with an an error if anything went wrong.
 */
OpenPgpService.prototype.restoreFromSecretBackupCode = function(
    code, email, token, progress) {

  // Values filled in by various async methods.
  /** {e2e.ByteArray} */
  var seed = null; // Holds a seed extracted from the code.
  /** {e2e.openpgp.Key} */
  var keyserverPublicKey = null; // Key (if any) hosted at the keyserver.

  // 1. Check the code is valid by converting it back into the
  // seed. The code contains a checksum, and a successful decode is
  // assumed to mean the user has entered a valid code they want to
  // use for the account.
  progress.status = 'Checking code...';
  return this.getSeedFromBackupCode_(code).then(goog.bind(function(data) {
    seed = data;
    // 2. Fetch the remote key if available.
    progress.status = 'Checking your Safe Mail account...';
    return this.searchPublicKey(email, true);
  }, this)).then(goog.bind(function(key) {
    keyserverPublicKey = key;
    // 3. By this stage, we know we have a valid code. We assume the
    // user wants to reset the account to using this private key,
    // regardless of anything existing state. So we delete all keys
    // under this email, and regenerate the key.
    return this.removeAllKeys_(email);
  }, this)).then(goog.bind(function() {
    // 4. Generate key from seed, which also updates the keyring.
    progress.status = 'Recovering account...';
    return this.generateKeysFromSeed_(
        /** @type {!e2e.ByteArray} */(seed), email);
  }, this)).then(goog.bind(function(key) {
    // 5. Upload new key to the keyserver. We pass in the previously
    // located key to avoid re-uploading the same key unless necessary.
    return this.uploadKey_(key, token, progress, keyserverPublicKey);
  }, this)).finally(function() {
    progress.status = null;
  });

};


/**
 * Given a public key, decides if we need to verify it again from the
 * key server.
 * @param {!e2e.openpgp.Key} key The key to be verified.
 * @return {boolean} True if we should refetch this key.
 * @private
 */
OpenPgpService.prototype.shouldVerify_ = function(key) {
  // Note: arrays are converted to comma separated values in its
  // toString() representation. eg: [1, 2].toString() => "1,2"
  var last =
      this.publicKeyVerificationTimestamps_[key.key.fingerprint.toString()];
  if (!goog.isNumber(last)) {
    return true;
  }
  return ((Date.now() - last) > OpenPgpService.PUBLIC_KEY_REFETCH_MSEC_);
};


/**
 * Marks this public key as having been verified now.
 * @param {!e2e.openpgp.Key} key The key to be marked.
 * @private
 */
OpenPgpService.prototype.setVerified_ = function(key) {
  // Note: arrays are converted to comma separated values in its
  // toString() representation. eg: [1, 2].toString() => "1,2"
  this.publicKeyVerificationTimestamps_[key.key.fingerprint.toString()] =
      Date.now();
};


/**
 * Given a seed, return a promise that restores a private key from
 * the seed and returns the public key.
 * @param {!e2e.ByteArray} seed A seed for the backup recovery.
 * @param {string} email An email address to associated with the key.
 * @return {!angular.$q.Promise<!e2e.openpgp.Key>}
 * @private
 */
OpenPgpService.prototype.generateKeysFromSeed_ = function(seed, email) {
  var deferred = this.q_.defer();

  this.context_.restoreKeyring({
        seed: seed,
        count: 2 // We only manage one private key in the app.
  }, this.toUserId_(email)).addCallbacks(
      deferred.resolve, deferred.reject, deferred);

  // Refetch the key locally, and return that promise.
  return deferred.promise.then(goog.bind(function() {
    return this.searchPublicKey(email, false);
  }, this));
};


/**
 * Given a code, return a promise that decodes it and does
 * some basic sanity checks.
 * @param {string} code A recovery code.
 * @return {!angular.$q.Promise<!e2e.ByteArray>} A bytearray representing
 *     the seed for the backup recovery method in the context.
 * @private
 */
OpenPgpService.prototype.getSeedFromBackupCode_ = function(code) {
  var deferred = this.q_.defer();

  try {
    var seed = e2email.util.RecoveryCode.decode(code);
    if (seed.length !== e2e.openpgp.KeyGenerator.ECC_SEED_SIZE) {
      deferred.reject(new e2e.error.InvalidArgumentsError(
          this.translateService_.getMessage('recoveryCodeTooShortError')));
    } else {
      deferred.resolve(seed);
    }
  } catch (e) {
    deferred.reject(e);
  }
  return deferred.promise;
};


/**
 * Given an email, return a promise that removes all local keys
 * (private or public) from the keyring.
 * @param {string} email An email address to search.
 * @return {!angular.$q.Promise}
 * @private
 */
OpenPgpService.prototype.removeAllKeys_ = function(email) {
  // Local keys are indexed by their bracketed id.
  this.context_.deleteKey(this.toUserId_(email));
  // TODO: context needs to return an async result to avoid
  // race conditions.
  return this.q_.when(undefined);
};


/**
 * Retrieves the requested public key, and saves it locally if this is
 * the first time. Otherwise, it rechecks the fingerprint against the
 * remote key server (with a suitable interval between checks.)
 * @param {string} email The email address for the public key.
 * @return !angular.$q.Promise <? {
 *     local: e2e.openpgp.Key, remote: e2e.openpgp.Key } > A promise
 *     that returns the local and remote versions of the key if found,
 *     and null otherwise.
 */
OpenPgpService.prototype.getVerifiedPublicKey = function(email) {
  var localKey = null;
  var remoteKey = null;

  // When verifying keys, compare with the oldest locally stored key.
  return this.searchLocalKey_(email, true).then(goog.bind(function(key) {
    localKey = key;
    if (goog.isDefAndNotNull(key) && !this.shouldVerify_(key)) {
      // We have a local version, and we've been advised we need not
      // retest the key. Use the local version as the remote version.
      remoteKey = key;
    }
  }, this)).then(goog.bind(function() {
    if (goog.isDefAndNotNull(localKey) && goog.isDefAndNotNull(remoteKey)) {
      // Just return our result.
      return {local: localKey, remote: remoteKey};
    } else {
      // chain a sequence of promises that fetches the remote key, and
      // updates the request timestamp.
      return this.searchRemoteKey_(email).then(goog.bind(function(key) {
        remoteKey = key;
        if (goog.isDefAndNotNull(remoteKey)) {
          this.setVerified_(remoteKey);
          if (goog.isDefAndNotNull(localKey)) {
            return {local: localKey, remote: remoteKey};
          } else {
            return {local: remoteKey, remote: remoteKey};
          }
        } else {
          return null;
        }
      }, this));
    }
  }, this));

};


/**
 * Fetch a public key, either remotely or locally.
 * @param {string} email An email address to use for the search.
 * @param {boolean} remote A boolean set to true to request a search
 *     at the keyserver.
 * @return {!angular.$q.Promise<e2e.openpgp.Key>} A promise containing
 *     the key (if found) or null (if not found.)
 */
OpenPgpService.prototype.searchPublicKey = function(email, remote) {
  if (remote) {
    return this.searchRemoteKey_(email);
  } else {
    return this.searchLocalKey_(email);
  }
};


/**
 * Searches a remote keyserver for a key.
 * @param {string} email An email address to search.
 * @return {!angular.$q.Promise<e2e.openpgp.Key>} A promise
 *     with the key (if found) or null (if not found.)
 * @private
 */
OpenPgpService.prototype.searchRemoteKey_ = function(email) {
  // For keys generated by the app, the current implementation of
  // context effectively always does a remote search when a bare email
  // address is provided.
  // The reason is that the keyserver indexes with the bare email, while
  // the context indexes it as the uid (which is "<foo@bar.com>")
  return this.searchKeyUsingContext_(email);
};


/**
 * search the local store for a key.
 * @param {string} email An email address to search.
 * @param {boolean=} opt_first Use the first key if multiple keys are found,
 *     the default is to return the last key.
 * @return {!angular.$q.Promise<e2e.openpgp.Key>} A promise with the
 *     key (if found) or null (if not found.)
 * @private
 */
OpenPgpService.prototype.searchLocalKey_ = function(email, opt_first) {
  // For keys generated by the app, the current implementation of
  // context effectively always does a local search when a bare email
  // is surrounded by angle braces.
  // The reason is that the keyserver indexes with the bare email, while
  // the context indexes it as the uid (which is "<foo@bar.com>")
  return this.searchKeyUsingContext_(this.toUserId_(email), opt_first);
};


/**
 * Search for a public key using the context.
 * @param {string} id An identifier to use for the query.
 * @param {boolean=} opt_first Use the first key if multiple keys are found,
 *     the default is to return the last key.
 * @return {!angular.$q.Promise<e2e.openpgp.Key>} A promise with the
 *     key (if found) or null (if not found.)
 * @private
 */
OpenPgpService.prototype.searchKeyUsingContext_ = function(id, opt_first) {
  var deferred = this.q_.defer();
  this.context_.searchPublicKey(id).addCallbacks(function(keys) {
    if (goog.isDefAndNotNull(keys) && (keys.length > 0)) {
      // Keys get automatically appended to the local keystore
      // whenever a remote key changes, and we do a remote lookup. So,
      // always return the latest key, unless specifically asked
      // otherwise.
      var index = opt_first ? 0 : keys.length - 1;
      deferred.resolve(keys[index]);
    } else {
      deferred.resolve(null);
    }
  }, deferred.reject, deferred);
  return deferred.promise;
};


/**
 * Generates a new keypair, import them locally and upload it to the
 * keyserver.
 * @param {string} email An email address to associate with the key.
 * @param {string} token An OAuth token for authentication during upload.
 * @param {!e2email.util.Progress} progress An object for status notifications.
 * @return {!angular.$q.Promise}
 */
OpenPgpService.prototype.generateKey = function(email, token, progress) {

  // First delete any locally stored keys under this email address.
  this.context_.deleteKey(this.toUserId_(email));
  var deferred = this.q_.defer();

  // NB: this API in context indexes keys under "<email>" rather than
  // just "email" Also, parameters are chosen to match what the
  // contextimpl does when we use the restoreFromSecretBackupCode method.
  this.context_.generateKey(
      e2e.signer.Algorithm.ECDSA, 256, e2e.cipher.Algorithm.ECDH, 256,
      '', '', email, 0)
          .addCallbacks(deferred.resolve, deferred.reject, deferred);

  return deferred.promise.then(goog.bind(function(keys) {
    var found = this.getPublicKeyFromArray_(keys);
    return this.uploadKey_(found, token, progress);
  }, this));
};


/**
 * Uploads a local public key for the provided email address to the
 * keyserver.
 * @param {string} email An email address associated with the key.
 * @param {string} token An OAuth token for authentication during upload.
 * @param {!e2email.util.Progress} progress An object for status
 *     notifications.
 * @return {!angular.$q.Promise}
 */
OpenPgpService.prototype.publishKey = function(email, token, progress) {
  return this.searchPublicKey(email, false).then(goog.bind(function(key) {
    // The key must exist.
    if (!goog.isDefAndNotNull(key)) {
      throw new e2e.error.InvalidArgumentsError(
          'Unexpected - public key to publish doesn\'t exist locally.');
    }
    return this.uploadKey_(key, token, progress);
  }, this));
};


/**
 * Encrypt and sign plaintext.
 * @param {string} plaintext The plaintext.
 * @param {!Array.<!e2e.openpgp.Key>} encryptionKeys The keys to
 *     encrypt the message with.
 * @param {e2e.openpgp.Key} signingKey The private key to sign with.
 * @return {!angular.$q.Promise<string>} A promise that returns with
 *     ciphertext if the operation was successful.
 */
OpenPgpService.prototype.encryptSign = function(
    plaintext, encryptionKeys, signingKey) {
  var deferred = this.q_.defer();
  this.context_.encryptSign(
      plaintext, {}, encryptionKeys, [], signingKey)
          .addCallbacks(deferred.resolve, deferred.reject, deferred);
  return deferred.promise;
};


/**
 * Decrypt and verify armored ciphertext.
 * @param {string} ciphertext The armored ciphertext.
 * @param {e2e.openpgp.Key} decryptKey The private key to
 *     decrypt the message with.
 * @param {!e2e.openpgp.Key} verifyKey The public key that the
 *     message should be signed with.
 * @return {!angular.$q.Promise<
 *     e2email.components.openpgp.OpenPgpService.DecryptResult>} A promise
 *     that returns with plaintext if the operation was successful, and
 *     a warning if it was not signed by verifyKey.
 */
OpenPgpService.prototype.decryptVerify = function(
    ciphertext, decryptKey, verifyKey) {

  var deferred = this.q_.defer();
  var warning = null;
  this.context_.verifyDecrypt(function(ignore) {
    throw new e2e.error.UnsupportedError(
        'Does not support symmetric encrypted messages.');
  }, ciphertext).addCallbacks(function(result) {
    // Verify that we have a signature, and that the signature
    // matches our expected key.
    // plaintext.
    if (!goog.isDefAndNotNull(result.decrypt) ||
        !goog.isDefAndNotNull(result.decrypt.data) ||
        !goog.isDefAndNotNull(result.decrypt.options)) {
      deferred.reject(new e2e.error.InvalidArgumentsError(
          this.translateService_.getMessage('invalidDecryptionContentError')));
    } else {
      if (!this.hasGoodSignature_(result.verify, verifyKey)) {
        warning = 'invalidSignatureError';
      }
      deferred.resolve(result.decrypt);
    }
  }, goog.bind(deferred.reject, deferred), this);

  // Return a chained promise that (on success) converts the bytearray
  // into a DecryptResult.
  return deferred.promise.then(goog.bind(function(decrypt) {
    var defer = this.q_.defer();
    e2e.byteArrayToStringAsync(decrypt.data, decrypt.options.charset)
        .addCallbacks(function(content) {
          defer.resolve({ 'content': content, 'warning': warning });
        }, defer.reject, defer);
    return defer.promise;
  }, this));
};


/**
 * Given a decryption result and an expected signing key,
 * verify that there's at least one good signature from
 * the signing key.
 * @param {?e2e.openpgp.VerifyResult} verify The result of
 *     a decryptVerify operation.
 * @param {!e2e.openpgp.Key} verifyKey The public key whose
 *     signature we're expecting to find in the result.
 * @return {boolean} Return true if we found a good signature
 *     from the verifying key.
 * @private
 */
OpenPgpService.prototype.hasGoodSignature_ = function(verify, verifyKey) {
  if (!goog.isDefAndNotNull(verify) || !goog.isArray(verify.success) ||
      !goog.isArray(verify.failure) || verify.success.length === 0) {
    return false;
  }

  return goog.array.some(verify.success, function(key) {
    return goog.array.equals(key.key.fingerprint, verifyKey.key.fingerprint);
  });
};


/**
 * Given an array of keys, check there's just one public key,
 * and return it. Otherwise, throw an error.
 * @param {!e2e.openpgp.Keys} keys The array of keys to check.
 * @return {!e2e.openpgp.Key} The one public key in it.
 * @private
 */
OpenPgpService.prototype.getPublicKeyFromArray_ = function(keys) {
  /** @type {?e2e.openpgp.Key} */
  var found = null;
  var count = 0;
  goog.array.forEach(keys, function(keyinfo) {
    if (!keyinfo['key']['secret']) {
      count++;
      found = keyinfo;
    }
  });
  if (count !== 1) {
    throw new e2e.error.InvalidArgumentsError(
        'Unexpected - need one public key (found ' + count + ')');
  }
  // Should be present.
  if (goog.isNull(found)) {
    throw new e2e.error.InvalidArgumentsError('Unexpected - missing key.');
  } else {
    return /** @type {!e2e.openpgp.Key} */(found);
  }
};


/**
 * Upload a public key to the keyserver under the current logged-in
 * user's identity.
 * @param {e2e.openpgp.Key} uploadKey The key to upload.
 * @param {string} token An OAuth token for authentication.
 * @param {e2email.util.Progress} progress A progress object for
 *     status updates.
 * @param {e2e.openpgp.Key=} opt_keyserverKey An existing key on the
 *     keyserver - can be used avoid keyserver update if the newly
 *     generated key has the same fingerprint as the keyserver key.
 * @return {!angular.$q.Promise}
 * @private
 */
OpenPgpService.prototype.uploadKey_ = function(
    uploadKey, token, progress, opt_keyserverKey) {
  var deferred = this.q_.defer();

  if (!goog.isDefAndNotNull(uploadKey)) {
    deferred.reject(new e2e.error.InvalidArgumentsError(
        'Unexpected - no public key submitted for upload.'));
  } else if (!goog.isDefAndNotNull(uploadKey['serialized'])) {
    deferred.reject(new e2e.error.InvalidArgumentsError(
        'Unexpected - public key has no serialized content.'));
  } else if (goog.isDefAndNotNull(opt_keyserverKey) &&
      goog.array.equals(uploadKey['key']['fingerprint'],
                        opt_keyserverKey['key']['fingerprint'])) {
    // Local and remote keys are the same, so nothing to do.
    // indicate this by resolving a null in the promise.
    deferred.resolve(null);
  } else {
    deferred.resolve(uploadKey['serialized']);
  }
  return deferred.promise.then(goog.bind(function(serialized) {
    if (!goog.isDefAndNotNull(serialized)) {
      // Nothing to do, just return.
      return this.q_.when(undefined);
    }
    progress.status = 'Update Safe Mail account info...';
    return this.http_.post(
        OpenPgpService.KEYSERVER_URL + '/pks/oauthadd', {
          'token': token,
          'keytext': e2e.openpgp.asciiArmor.encode(
              'PUBLIC KEY BLOCK', serialized)
        }, {
          headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
          transformRequest: function(params) {
            var content = [];
            for (var key in params) {
              if (params.hasOwnProperty(key)) {
                content.push(encodeURIComponent(key) + '=' +
                    encodeURIComponent(params[key]));
              }
            }
            return content.join('&');
          }
        });
  }, this));
};


/**
 * Given an email, surround it with angle braces to form the User ID.
 * @param {string} email An email to convert to a User ID.
 * @return {string} The User ID.
 * @private
 */
OpenPgpService.prototype.toUserId_ = function(email) {
  return '<' + email + '>';
};


/**
 * Angular module.
 * @type {!angular.Module}
 */
e2email.components.openpgp.module = angular
    .module('e2email.components.openpgp.OpenPgpService', [])
    .service('openpgpService', OpenPgpService);

});  // goog.scope
